/*****************************************************************
 *   Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 ****************************************************************/
package org.epiphron.convert;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.epiphron.BeanConverter;
import org.epiphron.ConversionMatching;
import org.epiphron.EpiphronException;
import org.semanticweb.owlapi.model.IRI;
import org.semanticweb.owlapi.model.OWLAnnotationProperty;
import org.semanticweb.owlapi.model.OWLAxiom;
import org.semanticweb.owlapi.model.OWLClassExpression;
import org.semanticweb.owlapi.model.OWLDataFactory;
import org.semanticweb.owlapi.model.OWLDataProperty;
import org.semanticweb.owlapi.model.OWLDataRange;
import org.semanticweb.owlapi.model.OWLDatatype;
import org.semanticweb.owlapi.model.OWLIndividual;
import org.semanticweb.owlapi.model.OWLNamedIndividual;
import org.semanticweb.owlapi.model.OWLObjectProperty;
import org.semanticweb.owlapi.model.OWLOntology;
import org.semanticweb.owlapi.model.OWLOntologyManager;
import org.semanticweb.owlapi.model.OWLProperty;
import org.semanticweb.owlapi.model.OWLPropertyAssertionAxiom;

public class DefaultBeanConverter implements BeanConverter {
	public static final String JAVACLASS = "http://code.google.com/p/epiphron/javaclass";
	public static final String JAVAFIELD = "http://code.google.com/p/epiphron/javafield";
	public static final String SEQUENCE = "http://code.google.com/p/epiphron/sequence";
	
	private DefaultConversionMatching matching;
	
	private OWLOntology ontology;
	private OWLOntologyManager manager;
	private OWLDataFactory dataFactory;
	private OWLAnnotationProperty javaclass;
	private OWLAnnotationProperty javafield;
	
	/**
	 * List of objects that will be used to close the world if needed
	 */
	private List<PendingWorldClosure> pendingClosures;
		
	/**
	 * Converts specified beans to RDF and saves
	 * them in given ontology
	 * @param ontology the ontology to store information
	 * @param beans objects to convert
	 */
	public ConversionMatching convert(OWLOntology ontology, Object... beans) throws EpiphronException {
		this.ontology = ontology;
		this.manager = ontology.getOWLOntologyManager();
		this.dataFactory = manager.getOWLDataFactory();
		javaclass = dataFactory.getOWLAnnotationProperty(IRI.create(JAVACLASS));
		javafield = dataFactory.getOWLAnnotationProperty(IRI.create(JAVAFIELD));
		
		pendingClosures = new ArrayList<PendingWorldClosure>();
		
		matching = new DefaultConversionMatching();
		
		for (Object bean : beans) {
			convert(bean);
		}
		return matching;
	}

	/**
	 * Converts a bean to individual and returns it
	 */
	OWLNamedIndividual convert(Object bean) {
		return _convert(bean, false);
	}

	private OWLNamedIndividual _convert(Object bean, boolean shallow) {
		return (matching.contains(bean)) ? existing(bean) : write(bean,
					create(bean), shallow);
	}

	private OWLNamedIndividual create(Object bean) {
		if (!ontology.containsClassInSignature(IRI.create(getURI(bean)))) {
			return null;
		}
		
		OWLNamedIndividual individual = existing(bean);
		manager.addAxiom(ontology, 
			dataFactory.getOWLClassAssertionAxiom(getRDFSClass(bean), individual));
		return individual;
	}
	
	private OWLNamedIndividual write(Object bean, OWLNamedIndividual subject, boolean shallow) {
		if (subject == null) {
			return null;
		}
		
		matching.add(bean, subject);
		
		boolean forceDeep = false;
		for (ValuesContext p : TypeWrapper.valueContexts(bean, getPrefix())) {
			if (!(shallow && p.type().isAssignableFrom(Collection.class)) || forceDeep) {
				saveOrUpdate(subject, p);
			}
		}
		return subject;
	}
	
	private void saveOrUpdate(OWLNamedIndividual subject, ValuesContext pc) {
		Object o = pc.invokeGetter();
		OWLProperty<?, ?> property = getRDFProperty(pc);
		
		manager.removeAxioms(ontology, findAxioms(ontology, subject, property));
		
		if (o != null) {
			if (Iterable.class.isAssignableFrom(pc.type())) {
				saveIterable(subject, property, (Iterable<?>) o);
				addFieldAnnotation(pc);
				if (property instanceof OWLObjectProperty 
						&& canCloseWorld(ontology, (OWLObjectProperty) property)) {
					pendingClosures.add(new PendingWorldClosure(pc));
				}
			}
			else {
				if (saveSingle(subject, property, o)) {
					addFieldAnnotation(pc);
				}
			}
		}
	}

	/**
	 * @return whether world for this particular property can be closed
	 */
	protected boolean canCloseWorld(OWLOntology ontology, OWLObjectProperty property) {
		return true;
	}

	private void addFieldAnnotation(ValuesContext pc) {
		manager.addAxiom(ontology, dataFactory.getOWLAnnotationAssertionAxiom(IRI.create(pc.uri()), 
			dataFactory.getOWLAnnotation(javafield, dataFactory.getOWLLiteral(pc.getName()))));
	}
	
	private Set<OWLAxiom> findAxioms(OWLOntology ontology, OWLNamedIndividual subject, OWLProperty<?, ?> property) {
		Set<OWLAxiom> axioms = new HashSet<OWLAxiom>();
		axioms.addAll(ontology.getObjectPropertyAssertionAxioms(subject));
		axioms.addAll(ontology.getDataPropertyAssertionAxioms(subject));
		
		for (Iterator<OWLAxiom> axiomIt = axioms.iterator(); axiomIt.hasNext();) {
			OWLPropertyAssertionAxiom<?, ?> axiom = (OWLPropertyAssertionAxiom<?, ?>) axiomIt.next();
			if (!axiom.getProperty().equals(property)) {
				axiomIt.remove();
			}
		}
		return axioms;
	}
	
	/**
	 * returns an existing OntClass or creates a new one adding an important
	 * annotation indicating the original java class.
	 * 
	 * @param bean
	 *            the bean we are saving or updating to the triple store
	 * @return
	 */
	private OWLClassExpression getRDFSClass(Object bean) {
		String uri = getURI(bean);
		
		manager.addAxiom(ontology, dataFactory.getOWLAnnotationAssertionAxiom(IRI.create(uri), 
				dataFactory.getOWLAnnotation(javaclass, dataFactory.getOWLLiteral(bean.getClass().getName()))));
		
		return dataFactory.getOWLClass(IRI.create(uri));
	}
	
	private OWLProperty<?, ?> getRDFProperty(ValuesContext ctx) {
		OWLProperty<?, ?> property;
		IRI iri = IRI.create(ctx.uri());
		
		if (PrimitiveWrapper.isPrimitive(ctx.type())) {
			property = dataFactory.getOWLDataProperty(iri);
		}
		else {
			property = dataFactory.getOWLObjectProperty(iri);
		}
		
		return property;
	}

	private OWLNamedIndividual existing(Object bean) {
		return dataFactory.getOWLNamedIndividual(IRI.create(instanceURI(bean)));
	}
	
	private String getURI(Object bean) {
		return TypeWrapper.type(bean, getPrefix()).typeUri();
	}
	
	/**
	 * @return ontology prefix for individuals
	 */
	private String getPrefix() {
		return ontology.getOntologyID().getOntologyIRI().toString() + "#";
	}

	private String instanceURI(Object bean) {
		return TypeWrapper.type(bean, getPrefix()).uri(bean);
	}
	
	void saveIterable(OWLNamedIndividual subject, OWLProperty<?, ?> property, Iterable<?> c) {
		
		for (Object obj : c) {
			saveSingle(subject, property, obj);
		}
		
	}
	
	boolean saveSingle(OWLNamedIndividual subject, OWLProperty<?, ?> property, Object obj) {
		OWLOntologyManager manager = ontology.getOWLOntologyManager();
		OWLDataFactory factory = manager.getOWLDataFactory();
		
		if (PrimitiveWrapper.isPrimitive(obj) && property instanceof OWLDataProperty) {
			// finding a range datatype
			Set<OWLDataRange> ranges = ((OWLDataProperty) property).getRanges(ontology);
			OWLDatatype dataType = null;
			
			for (OWLDataRange range : ranges) {
				if (range.isDatatype()) {
					dataType = range.asOWLDatatype();
					break;
				}
			}
			
			manager.addAxiom(ontology, factory.getOWLDataPropertyAssertionAxiom(
					(OWLDataProperty) property, subject, ConversionUtil.toLiteral(factory, dataType, obj)));
			return true;
		}
		else if (property instanceof OWLObjectProperty) {
			OWLIndividual individual = _convert(obj, true);
			
			if (individual != null) {
				manager.addAxiom(ontology, factory.getOWLObjectPropertyAssertionAxiom(
					(OWLObjectProperty) property, subject, individual));
				return true;
			}
		}
		return false;
	}

	public void closeWorld(OWLOntology ontology) {
		for (PendingWorldClosure closure : pendingClosures) {
			closure.closeWorld(ontology);
		}
	}
	
	/**
	 * Storage of data that can be used to close the world.
	 * The axioms themselves aren't stored, because cardinality
	 * can change after reasoning
	 */
	class PendingWorldClosure {
		ValuesContext property;
		
		PendingWorldClosure(ValuesContext property) {
			this.property = property;
		}
		
		/**
		 * Closes the world for this particular property.
		 * @param ontology
		 */
		public void closeWorld(OWLOntology ontology) {
			Object value = property.invokeGetter();
			int size;
			
			if (value instanceof Collection<?>) {
				size = ((Collection<?>) value).size();
			}
			else if (value instanceof Array) {
				size = Array.getLength(value);
			}
			else {
				return;
			}
			
			OWLOntologyManager manager = ontology.getOWLOntologyManager();
			OWLIndividual individual = existing(property.getSubject());
			
			OWLClassExpression clazz = 
				manager.getOWLDataFactory().getOWLObjectExactCardinality(size, 
					(OWLObjectProperty) getRDFProperty(property));
			
			manager.addAxiom(ontology, 
				manager.getOWLDataFactory().getOWLClassAssertionAxiom(clazz, individual));
		}
	}
}
